// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.ContractsLight;
using System.Threading;
using System.Threading.Tasks;
using BuildXL.Cache.ContentStore.Exceptions;
using BuildXL.Cache.ContentStore.FileSystem;
using BuildXL.Cache.ContentStore.Interfaces.FileSystem;
using BuildXL.Cache.ContentStore.Interfaces.Logging;
using BuildXL.Cache.ContentStore.Interfaces.Time;
using BuildXL.Cache.ContentStore.Interfaces.Tracing;
using BuildXL.Cache.ContentStore.Logging;
using BuildXL.Cache.ContentStore.Tracing;
using BuildXL.Cache.MemoizationStore.Sessions;
using BuildXL.Cache.MemoizationStore.Stores;
using CLAP;

// ReSharper disable UnusedMember.Global
namespace BuildXL.Cache.MemoizationStore.App
{
    /// <summary>
    ///     Core application implementation with CLAP verbs.
    /// </summary>
    internal sealed partial class Application : IDisposable
    {
        private readonly CancellationToken _token = default(CancellationToken);
        private readonly IAbsFileSystem _fileSystem;
        private readonly ConsoleLog _consoleLog;
        private readonly Logger _logger;
        private readonly Tracer _tracer;
        private bool _waitForDebugger;
        private FileLog _fileLog;
        private bool _logAutoFlush;
        private string _logDirectoryPath;
        private long _logMaxFileSize;
        private int _logMaxFileCount;
        private long _maxRowCount = long.MaxValue;
        private bool _waitForLruOnShutdown = true;

        /// <summary>
        ///     Initializes a new instance of the <see cref="Application"/> class.
        /// </summary>
        public Application()
        {
            _consoleLog = new ConsoleLog(Severity.Warning);
            _logger = new Logger(true, _consoleLog);
            _fileSystem = new PassThroughFileSystem(_logger);
            _tracer = new Tracer(nameof(Application));
        }

        /// <inheritdoc />
        [SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "_fileLog")]
        public void Dispose()
        {
            _logger.Dispose();
            _fileLog?.Dispose();
            _consoleLog.Dispose();
            _fileSystem.Dispose();
        }

        /// <summary>
        ///     Show user help.
        /// </summary>
        /// <param name="help">Help string generated by CLAP.</param>
        /// <remarks>
        ///     This is intended to only be called by CLAP.
        /// </remarks>
        [Help(Aliases = "help,h,?")]
        public void ShowHelp(string help)
        {
            Contract.Requires(help != null);
            _logger.Always("MemoizationStore Tool");
            _logger.Always(help);
        }

        /// <summary>
        ///     Handle verb exception.
        /// </summary>
        /// <remarks>
        ///     This is intended to only be called by CLAP.
        /// </remarks>
        [Error]
        public void HandleError(ExceptionContext exceptionContext)
        {
            Contract.Requires(exceptionContext != null);
            _logger.Error(exceptionContext.Exception.InnerException != null
                ? $"{exceptionContext.Exception.Message}: {exceptionContext.Exception.InnerException.Message}"
                : exceptionContext.Exception.Message);
            exceptionContext.ReThrow = false;
        }

        /// <summary>
        ///     Set option to wait for debugger to attach.
        /// </summary>
        [Global("WaitForDebugger", Description = "Wait for debugger to attach")]
        public void SetWaitForDebugger(bool waitForDebugger)
        {
            _waitForDebugger = waitForDebugger;
        }

        /// <summary>
        ///     Set the console log line format to short or long form.
        /// </summary>
        [Global("LogLongForm", Description = "Use long logging form on console")]
        public void SetLogLongLayout(bool value)
        {
            foreach (var consoleLog in _logger.GetLog<ConsoleLog>())
            {
                consoleLog.UseShortLayout = !value;
            }
        }

        /// <summary>
        ///     Set the console log severity filter.
        /// </summary>
        [Global("LogSeverity", Description = "Set console severity filter")]
        public void SetLogSeverity(Severity logSeverity)
        {
            foreach (var consoleLog in _logger.GetLog<ConsoleLog>())
            {
                consoleLog.CurrentSeverity = logSeverity;
            }
        }

        /// <summary>
        ///     Enable automatic log file flushing.
        /// </summary>
        [Global("LogAutoFlush", Description = "Enable automatic log file flushing")]
        public void SetLogAutoFlush(bool logAutoFlush)
        {
            _logAutoFlush = logAutoFlush;
        }

        /// <summary>
        ///     Self explanatory.
        /// </summary>
        [Global("LogDirectoryPath", Description = "Set log directory path")]
        public void SetLogDirectoryPath(string path)
        {
            _logDirectoryPath = path;
        }

        /// <summary>
        ///     Set log rolling max file size.
        /// </summary>
        [Global("LogMaxFileSizeMB", Description = "Set log rolling max file size in MB")]
        public void SetLogMaxFileSizeMB(long value)
        {
            _logMaxFileSize = value * 1024 * 1024;
        }

        /// <summary>
        ///     Set log rolling max file count.
        /// </summary>
        [Global("LogMaxFileCount", Description = "Set log rolling max file count")]
        public void SetLogMaxFileCount(int value)
        {
            _logMaxFileCount = value;
        }

        /// <summary>
        ///     Set maximum number of content hash lists stored.
        /// </summary>
        [Global("MaxRowCount", Description = "Maximum number of content hash lists stored")]
        public void SetMaxRowCount(long value)
        {
            _maxRowCount = value;
        }

        /// <summary>
        ///     Set flag whether to wait for LRU touches on shutdown.
        /// </summary>
        [Global("WaitForLruOnShutdown", Description = "Wait for LRU touches on shutdown")]
        public void SetWaitForLruOnShutdown(bool value)
        {
            _waitForLruOnShutdown = value;
        }

        private void Initialize()
        {
            if (_waitForDebugger)
            {
                _logger.Warning("Waiting for debugger to attach. Hit any key to bypass.");

                while (!Debugger.IsAttached)
                {
                    Thread.Sleep(TimeSpan.FromSeconds(1));
                }

                Debugger.Break();
            }

            SetThreadPoolSizes();

            _fileLog = new FileLog(_logDirectoryPath, null, Severity.Diagnostic, _logAutoFlush, _logMaxFileSize, _logMaxFileCount);
            _logger.AddLog(_fileLog);
        }

        private void SetThreadPoolSizes()
        {
            int workerThreads;
            int completionPortThreads;

            ThreadPool.GetMaxThreads(out workerThreads, out completionPortThreads);
            workerThreads = Math.Max(workerThreads, Environment.ProcessorCount * 16);
            ThreadPool.SetMaxThreads(workerThreads, completionPortThreads);

            ThreadPool.GetMinThreads(out workerThreads, out completionPortThreads);
            workerThreads = Math.Max(workerThreads, Environment.ProcessorCount * 16);
            ThreadPool.SetMinThreads(workerThreads, completionPortThreads);
        }

        private void RunSQLiteStoreSession(
            AbsolutePath rootPath,
            bool lruEnabled,
            System.Func<Context, SQLiteMemoizationStore, SQLiteMemoizationSession, Task> funcAsync)
        {
            var config = new SQLiteMemoizationStoreConfiguration(rootPath)
            {
                MaxRowCount = lruEnabled ? _maxRowCount : 0,
                WaitForLruOnShutdown = _waitForLruOnShutdown,
            };
            System.Func<SQLiteMemoizationStore> createFunc = () => new SQLiteMemoizationStore(
                _logger,
                SystemClock.Instance,
                config);
            RunMemoizationStore(createFunc, funcAsync);
        }

        private void RunMemoizationStore
            (
            System.Func<SQLiteMemoizationStore> createStoreFunc,
            System.Func<Context, SQLiteMemoizationStore, SQLiteMemoizationSession, Task> funcAsync
            )
        {
            Initialize();
            var context = new Context(_logger);

            try
            {
                using (var store = createStoreFunc())
                {
                    try
                    {
                        var startupResult = store.StartupAsync(context).Result;
                        if (!startupResult.Succeeded)
                        {
                            throw new CacheException("Failed to start store, error=[{0}]", startupResult.ErrorMessage);
                        }

                        var createSessionResult = store.CreateSession(context, "tool");
                        if (!createSessionResult.Succeeded)
                        {
                            throw new CacheException("Failed to create session, error=[{0}]", createSessionResult.ErrorMessage);
                        }

                        using (var session = (SQLiteMemoizationSession)createSessionResult.Session)
                        {
                            try
                            {
                                var sessionBoolResult = session.StartupAsync(context).Result;
                                if (!sessionBoolResult.Succeeded)
                                {
                                    throw new CacheException("Failed to start session, error=[{0}]", createSessionResult.ErrorMessage);
                                }

                                funcAsync(context, store, session).Wait(_token);
                            }
                            finally
                            {
                                var sessionBoolResult = session.ShutdownAsync(context).Result;
                                if (!sessionBoolResult.Succeeded)
                                {
                                    context.Error($"Failed to shutdown session, error=[{sessionBoolResult.ErrorMessage}]");
                                }
                            }
                        }
                    }
                    finally
                    {
                        var r = store.ShutdownAsync(context).Result;
                        if (!r.Succeeded)
                        {
                            context.Error($"Failed to shutdown store, error=[{r.ErrorMessage}]");
                        }
                    }
                }
            }
            catch (AggregateException exception)
            {
                _tracer.Error(context, exception.InnerException?.Message);
            }
        }
    }
}
